#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# this lesson is based on python 3

from datetime import datetime, timedelta, timezone
'''
注意到datetime是模块，datetime模块还包含一个datetime类，通过from datetime import datetime导入的才是datetime这个类。
如果仅导入import datetime，则必须引用全名datetime.datetime
'''
now = datetime.now() # 20xx-05-18 16:28:07.198690

'''
我们把1970年1月1日 00:00:00 UTC+00:00时区的时刻称为epoch time，记为0（1970年以前的时间timestamp为负数），
当前时间就是相对于epoch time的秒数，称为timestamp。
'''
timestp = now.timestamp() # Python的timestamp是一个浮点数，整数位表示秒。

now == datetime.fromtimestamp(timestp)
# timestamp是一个浮点数，它没有时区的概念，而datetime是有时区的。上述转换是在timestamp和本地时间做转换。
utc = datetime.utcfromtimestamp(timestp) # timestamp to utc time

cday = datetime.strptime('2015-6-1 18:19:59', '%Y-%m-%d %H:%M:%S') 
# transform formated string to datetime obj

print(now.strftime('%a, %b %d %H:%M'))
# print out datetime obj as formated str

ten_hours_later = now + timedelta(hours=10)
a_day_ago = now - timedelta(days=1)
# use timedelta to modify date and time

# datetime类型有一个时区属性tzinfo，但是默认为None，
# 所以无法区分这个datetime到底是哪个时区，除非强行给datetime设置一个时区
tz_utc_8 = timezone(timedelta(hours=8)) # 创建时区UTC+8:00
dt = now.replace(tzinfo=tz_utc_8) # 强制设置为UTC+8:00

utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc) # 拿到UTC时间，并强制设置时区为UTC+0:00
bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8))) # astimezone()将转换时区为北京时间
tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9))) # astimezone()将转换时区为东京时间
tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9))) # astimezone()将bj_dt转换时区为东京时间
# 如果要存储datetime，最佳方法是将其转换为timestamp再存储，因为timestamp的值与时区完全无关。


# collections:一个集合模块，提供了许多有用的集合类
from collections import namedtuple 

Point = namedtuple('Point', ['x', 'y'])
# namedtuple是一个函数，它用来创建一个自定义的tuple对象，并且规定了tuple元素的个数，
# 并可以用属性而不是索引来引用tuple的某个元素。
# 这样一来，我们用namedtuple可以很方便地定义一种数据类型，它具备tuple的不变性，又可以根据属性来引用，使用十分方便。
p = Point(1, 2)
isinstance(p, tuple) # True
print('p.x=', p.x)

from collections import deque # 高效实现插入和删除操作的双向列表，适合用于队列和栈

q = deque(['a', 'b', 'c'])
q.append('s')
q.appendleft('f')
q.pop()
q.popleft()

from collections import defaultdict # key不存在时，调用函数返回一个默认值的dict

dd = defaultdict(lambda : 'N/A')
dd['key1'] = 'value1'
print(dd['key1']) # 'value1'
print(dd['key2']) # 'N/A'

from collections import OrderedDict # 使用dict时，Key是无序的。做迭代时，无法确定Key的顺序。保持Key的顺序用

d = dict([('a', 1), ('b', 2), ('c', 3)])
od = OrderedDict([('x', 4), ('y', 5), ('z', 6)])
# OrderedDict的Key会按照插入的顺序排列，不是Key本身排序

from collections import ChainMap
import os, argparse
# ChainMap可以把一组dict串起来并组成一个逻辑上的dict。
# ChainMap本身也是一个dict，但是查找的时候，会按照顺序在内部的dict依次查找。

# 应用程序往往都需要传入参数，参数可以通过命令行传入，可以通过环境变量传入，还可以有默认参数。
# 我们可以用ChainMap实现参数的优先级查找，即先查命令行参数，如果没有传入，再查环境变量，如果没有，就使用默认参数。

# 构造缺省参数:
defaults = {
    'color': 'red',
    'user': 'guest'
}

# 构造命令行参数:
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
namespace = parser.parse_args()
command_line_args = { k: v for k, v in vars(namespace).items() if v }

# 组合成ChainMap:
combined = ChainMap(command_line_args, os.environ, defaults)

# 打印参数:
print('color=%s' % combined['color'])
print('user=%s' % combined['user'])

from collections import Counter # 一个简单的计数器, 实际上也是dict的一个子类

c = Counter()
for ch in 'programming': # get number of letters
    c[ch] = c[ch] + 1

print(c)
# Counter({'g': 2, 'm': 2, 'r': 2, 'a': 1, 'i': 1, 'o': 1, 'n': 1, 'p': 1})
c.update('hello')
print(c)
# Counter({'r': 2, 'o': 2, 'g': 2, 'm': 2, 'l': 2, 'p': 1, 'a': 1, 'i': 1, 'n': 1, 'h': 1, 'e': 1})


# Base64是一种用64个字符来表示任意二进制数据（格式文本、图像、音频等等）的方法
# Base64适用于小段内容的编码，比如数字证书签名、Cookie的内容等。
import base64

b64 = base64.b64encode(b'aBinaryFormatedText')
print(base64.b64decode(b64))

# 标准的Base64编码后可能出现字符+和/，在URL中就不能直接作为参数，
# 所以又有一种"url safe"的base64编码，其实就是把字符+和/分别变成-和_：
for_url = base64.urlsafe_b64encode(b'www.google.cn')
base64.urlsafe_b64decode(for_url)

import struct # 解决bytes和其他二进制数据类型的转换

# pack函数把任意数据类型变成bytes
test_bytes = struct.pack('>I', 10240009)
# pack的第一个参数是处理指令:
# >表示字节顺序是big-endian，也就是网络序，I表示4字节无符号整数

# unpack把bytes变成相应的数据类型：
test_tuple = struct.unpack('>IH',  b'\xf0\xf0\xf0\xf0\x80\x80')
# 根据>IH的说明，后面的bytes依次变为I：4字节无符号整数和H：2字节无符号整数

# 摘要算法又称哈希算法、散列算法。
# 它通过一个函数，把任意长度的数据转换为一个长度固定的数据串（通常用16进制的字符串表示）
import hashlib

md5 = hashlib.md5() # create an instance
md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
print(md5.hexdigest())
# MD5是最常见的摘要算法，速度很快，生成结果是固定的128 bit字节，通常用一个32位的16进制字符串表示。

sha1 = hashlib.sha1()
sha1.update('how to use sha1'.encode('utf-8'))
sha1.update(' in python hashlib?'.encode('utf-8')) # the same as 'how to use sha1 in python hashlib?'
print(sha1.hexdigest())
# SHA1的结果是160 bit字节，通常用一个40位的16进制字符串表示
# 更安全的算法是SHA256和SHA512，不过越安全的算法不仅越慢，而且摘要长度更长

# 正确的保存口令的方式是不存储用户的明文口令，而是存储用户口令的摘要
# 为了防止黑客通过彩虹表根据哈希值反推原始口令, 在计算哈希的时候，不能仅针对原始输入计算，
# 需要增加一个salt来使得相同的输入也能得到不同的哈希
# Hmac算法：Keyed-Hashing for Message Authentication。它通过一个标准算法，在计算哈希的过程中，把key混入计算过程中
# 针对所有哈希算法都通用，无论是MD5还是SHA-1
import hmac
message = b'my dear password'
key = b'why not salt' # 需要注意传入的key和message都是bytes类型
h = hmac.new(key, message, digestmod='MD5')
h.hexdigest() # hmac输出的长度和原始哈希算法的长度一致


import itertools # 提供了非常有用的用于操作迭代对象的函数

natural = itertools.count(1)
for n in natural:
    print(n)
    # count()会创建一个无限的迭代器，所以上述代码会打印出自然数序列，根本停不下来，只能按Ctrl+C退出。

cs = itertools.cycle('ABC')
for c in cs:
    print(c)
    # cycle()会把传入的一个序列无限重复下去: A B C A B C A B C A B C...

rp = itertools.repeat('A', 3)
for a in rp:
    print(a)
    # repeat()负责把一个元素无限重复下去，不过如果提供第二个参数就可以限定重复次数: A A A

ns = itertools.takewhile(lambda x: x < 10, natural)
# takewhile根据函数返回判断，来从序列中截取出一个有限的序列

for c in itertools.chain('ABC', 'XYZ'):
    print(c)
    # chain()可以把一组迭代对象串联起来，形成一个更大的迭代器：A B C X Y Z ...

for key, group in itertools.groupby('AaaBBbcCAAa', lambda c: c.upper()):
    print(key, list(group))
    # groupby()把迭代器中相邻的重复元素挑出来放在一起
    # 提供函数时，只要作用于函数的两个元素返回的值相等，这两个元素就被认为是在一组的，而函数返回值作为组的key
'''
A ['A', 'a', 'a']
B ['B', 'B', 'b']
C ['c', 'C']
A ['A', 'A', 'a']
'''


# with语句允许我们非常方便地使用资源，而不必担心资源没有关闭
with open('/path/to/file', 'r') as f:
    f.read()

# 并不是只有open()函数返回的fp对象才能使用with语句。实际上，任何对象，只要正确实现了上下文管理，就可以用于with语句
# 实现上下文管理是通过__enter__和__exit__这两个方法实现的。例如，下面的class实现了这两个方法：
class Query(object):
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print('Begin')
        return self
    
    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type:
            print('Error')
        else:
            print('end')
    
    def query(self):
        print('query about %s...' % self.name)

with Query('Alice') as ali:
    ali.query()

# 编写__enter__和__exit__仍然很繁琐，因此Python的标准库contextlib提供了更简单的写法:
from contextlib import contextmanager

class Query2(object):
    def __init__(self, name):
        self.name = name

    def query(self):
        print('query now about %s...' % self.name)
    
@contextmanager
def create_query(name):
    print('Begin')
    q = Query2(name)
    yield q # 用yield语句把with ... as var把变量输出出去
    print('end')

with create_query('Bob') as q:
    q.query()

# 很多时候，我们希望在某段代码执行前后自动执行特定代码，也可以用@contextmanager实现
@contextmanager
def tag(name):
    print('<%s>' % name)
    yield
    print('</%s>' % name)

with tag('h1'):
    print('hello world')
'''
    with语句首先执行yield之前的语句，因此打印出<h1>；
    yield调用会执行with语句内部的所有语句，因此打印出hello和world；
    最后执行yield之后的语句，打印出</h1>。
<h1>
hello world
</h1>
'''

# 如果一个对象没有实现上下文，我们就不能把它用于with语句。
# 这个时候，可以用closing()来把该对象变为上下文对象
from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('https://www.python.org')) as page:
    for line in page:
        print(line)


# urllib提供了一系列用于操作URL的功能
from urllib import request

# 发送一个GET请求到指定的页面，然后返回HTTP的响应
with request.urlopen('https://api.douban.com/v2/book/2129650') as f:
    data = f.read()
	print('Status:', f.status, f.reason)
	for k, v in f.getheaders():
	    print('%s: %s'% (k, v))
	print('Data:', data.decode('utf-8'))

# 如果我们要想模拟浏览器发送GET请求，就需要使用Request对象，
# 通过往Request对象添加HTTP头，我们就可以把请求伪装成浏览器。
# 例如，模拟iPhone 6去请求豆瓣首页：
req = request.Request('https://www.douban.com/')
req.add_header('User-agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
with request.urlopen(req) as f:
    print('Status:', f.status, f.reason)
	for k, v in f.getheaders():
	    print('%s: %s'% (k, v))
	print('Data:', data.decode('utf-8'))

# 如果要以POST发送一个请求，只需要把参数data以bytes形式传入。
# 我们模拟一个微博登录，先读取登录的邮箱和口令，然后按照weibo.cn的登录页的格式
# 以username=xxx&password=xxx的编码传入
from urllib import parse

print('Login to weibo.cn...')
email = input('email:')
passwd = input('password:') # 读取登录的邮箱和口令
login_data = parse.urlencode([
    ('username', email),
    ('password', passwd),
    ('entry', 'mweibo'),
    ('client_id', ''),
    ('savestate', '1'),
    ('ec', ''),
    ('pagerefer', 'https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')
])

req = request.Request('https://passport.weibo.cn/sso/login')
req.add_header('Origin', 'https://passport.weibo.cn')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
req.add_header('Referer', 'https://passport.weibo.cn/signin/login?entry=mweibo&res=wel&wm=3349&r=http%3A%2F%2Fm.weibo.cn%2F')

with request.urlopen(req, data=login_data.encode('utf-8')) as f: # 编码传入
    print('Status:', f.status, f.reason)
    for k, v in f.getheaders():
        print('%s: %s' % (k, v))
    print('Data:', f.read().decode('utf-8'))


# XML虽然比JSON复杂，在Web中应用也不如以前多了，不过仍有很多地方在用

# 操作XML有两种方法：DOM和SAX。
# DOM会把整个XML读入内存，解析为树，因此占用内存大，解析慢，优点是可以任意遍历树的节点。
# SAX是流模式，边读边解析，占用内存小，解析快，缺点是我们需要自己处理事件。
# 正常情况下，优先考虑SAX，因为DOM实在太占内存

# 在Python中使用SAX解析XML非常简洁，通常我们关心的事件是start_element，end_element和char_data，
# 准备好这3个函数，然后就可以解析xml了
from xml.parsers.expat import ParserCreate

class DefaultSaxHandler(object):
    def start_element(self, name, attrs):
	    print('sax:start_element: %s, attrs: %s'% (name, str(attrs))

	def end_element(self, name):
	    print('sax:end_element: %s'% name)
	
	def char_data(self, text):
	    print('sax:char_data: %s'% text)
		


xml = r'''<?xml version="1.0"?>
<ol>
    <li><a href="/python">Python</a></li>
    <li><a href="/ruby">Ruby</a></li>
</ol>
'''

handler = DefaultSaxHandler()
parser = ParserCreate()
parser.StartElementHandler = handler.start_element
parser.EndElementHandler = handler.end_element
parser.CharacterDataHandler = handler.char_data
parser.Parse(xml)

# 如果我们要编写一个搜索引擎，
# 第一步是用爬虫把目标网站的页面抓下来，
# 第二步就是解析该HTML页面，看看里面的内容到底是新闻、图片还是视频。

# 假设第一步已经完成了，第二步应该如何解析HTML呢？
# HTML本质上是XML的子集，但是HTML的语法没有XML那么严格，所以不能用标准的DOM或SAX来解析HTML。
# 好在Python提供了HTMLParser来非常方便地解析HTML，只需简单几行代码：
from html.parser import HTMLParser
from html.entities import name2codepoint

class MyHTMLParser(HTMLParser):
    def handle_starttag(self, tag, attrs):
        print('<%s>' % tag)

    def handle_endtag(self, tag):
        print('</%s>' % tag)

    def handle_startendtag(self, tag, attrs):
        print('<%s/>' % tag)

    def handle_data(self, data):
        print(data)

    def handle_comment(self, data):
        print('<!--', data, '-->')

    def handle_entityref(self, name): #特殊字符有两种，一种是英文表示的&nbsp;
        print('&%s;' % name)

    def handle_charref(self, name): # 一种是数字表示的&#1234;
        print('&#%s;' % name)

parser = MyHTMLParser()
parser.feed('''<html>
<head></head>
<body>
<!-- test html parser -->
    <p>Some <a href=\"#\">html</a> HTML&nbsp;tutorial...<br>END</p>
</body></html>''')
# feed()方法可以多次调用，也就是不一定一次把整个HTML字符串都塞进去，可以一部分一部分塞进去。

